package com.unionpay.upyzt.net;

import com.unionpay.upyzt.Upyzt;
import com.unionpay.upyzt.exception.APIConnectionException;
import com.unionpay.upyzt.exception.UpyztException;

import java.net.ConnectException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;

public abstract class HttpClient {
    /**
     * Maximum sleep time between tries to send HTTP requests after network failure.
     */
    public static final long maxNetworkRetriesDelay = 5000;

    /**
     * Minimum sleep time between tries to send HTTP requests after network failure.
     */
    public static final long minNetworkRetriesDelay = 500;

    /**
     * A value indicating whether the client should sleep between automatic request retries.
     */
    boolean networkRetriesSleep = true;

    /**
     * Logger for record request and response.
     */
    Logger logger;

    /**
     * Initializes a new instance of the {@link com.unionpay.upyzt.net.HttpClient} class.
     */
    public HttpClient() {
        this.logger = Upyzt.getLogger() == null ? new Logger.NoOpLogger() : Upyzt.getLogger();
    }

    /**
     * Sends the given request to Upyzt's API.
     *
     * @param request the request
     * @return the response
     * @throws UpyztException If the request fails for any reason
     */
    public abstract UpyztResponse request(UpyztRequest request) throws UpyztException;

    /**
     * Sends the given request to Upyzt's API, retrying the request in cases of intermittent
     * problems.
     *
     * @param request the request
     * @return the response
     * @throws UpyztException If the request fails for any reason
     */
    public UpyztResponse requestWithRetries(UpyztRequest request) throws UpyztException {
        APIConnectionException requestException;
        UpyztResponse response = null;
        int retry = 0;

        while (true) {
            requestException = null;

            try {
                this.logger.beforeRequest(request);
                long startAt = System.currentTimeMillis();
                response = this.request(request);
                this.logger.afterResponse(request, response, System.currentTimeMillis() - startAt);
            } catch (APIConnectionException e) {
                requestException = e;
            }

            if (!this.shouldRetry(retry, requestException, request, response)) {
                break;
            }

            retry += 1;

            try {
                Thread.sleep(this.sleepTime(retry));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        if (requestException != null) {
            throw requestException;
        }

        response.setNumRetries(retry);

        return response;
    }

    /**
     * Builds the value of the {@code User-Agent} header.
     *
     * @return a string containing the value of the {@code User-Agent} header
     */
    protected static String buildUserAgentString() {
        return String.format("Upyzt/v1 JavaBindings/%s", Upyzt.VERSION);
    }

    /**
     * Builds the value of the {@code X-Upyzt-Client-User-Agent} header.
     *
     * @return a string containing the value of the {@code X-Upyzt-Client-User-Agent} header
     */
    protected static String buildXUpyztClientUserAgentString() {
        String[] propertyNames = {
                "os.name",
                "os.version",
                "os.arch",
                "java.version",
                "java.vendor",
                "java.vm.version",
                "java.vm.vendor"
        };

        Map<String, String> propertyMap = new HashMap<>();
        for (String propertyName : propertyNames) {
            propertyMap.put(propertyName, System.getProperty(propertyName));
        }
        propertyMap.put("bindings.version", Upyzt.VERSION);
        propertyMap.put("lang", "Java");
        propertyMap.put("publisher", "Upyzt");

        return com.unionpay.upyzt.net.APIResource.GSON.toJson(propertyMap);
    }

    private boolean shouldRetry(
            int numRetries, UpyztException exception, UpyztRequest request, UpyztResponse response) {
        // Do not retry if we are out of retries.
        if (numRetries >= request.options.getMaxNetworkRetries()) {
            return false;
        }

        // Retry on connection error.
        if ((exception != null)
                && (exception.getCause() != null)
                && (exception.getCause() instanceof ConnectException)) {
            return true;
        }

        // Retry on 500, 503, and other internal errors.
        return (response != null) && (response.getResponseCode() >= 500);
    }

    private long sleepTime(int numRetries) {
        if (!networkRetriesSleep) {
            return 0;
        }

        long delay = (long) (minNetworkRetriesDelay * Math.pow(2, numRetries - 1));

        if (delay > maxNetworkRetriesDelay) {
            delay = maxNetworkRetriesDelay;
        }

        double jitter = ThreadLocalRandom.current().nextDouble(0.75, 1.0);
        delay = (long) (delay * jitter);

        if (delay < minNetworkRetriesDelay) {
            delay = minNetworkRetriesDelay;
        }

        return delay;
    }




}
